﻿using System;
using System.IO;
using System.Net;
using System.Threading;

namespace XamlTorrent.Managers.UriGetter.Downloaders
{
    class HttpDownloader : DownloaderBase
    {
        public HttpDownloader(Uri source, FileInfo fileInfo)
            : base(source, fileInfo)
        {
        }

        public override bool SupportsSegmentation
        {
            get { return true; }
        }

        protected override ContentHeaderData GetHeaderData()
        {
            HttpWebRequest httpReq = (HttpWebRequest)WebRequest.Create(base.Source);
            httpReq.Method = WebRequestMethods.Http.Head;

            HttpWebResponse httpResp = (HttpWebResponse)httpReq.GetResponse();
            string acceptRanges = httpResp.Headers[HttpResponseHeader.AcceptRanges];
            bool segment = (String.Compare(acceptRanges, "bytes", StringComparison.InvariantCultureIgnoreCase) == 0);

            ContentHeaderData header = new ContentHeaderData(httpResp.ContentLength, segment);

            return header;
        }

        protected override long DownloadSegment(FileSegment segment)
        {
            HttpWebRequest httpReq = (HttpWebRequest)WebRequest.Create(base.Source);
            httpReq.Method = WebRequestMethods.Http.Get;

            bool expectPartialContent = !segment.IsEntireFile;

            if (expectPartialContent)
            {
                // If segment length or offset cannot be cast to Int32 safely then
                // use a Reflections hack to add it with WebHeaderCollection.SetAddVerified (private).

                if (segment.Offset <= int.MaxValue && (segment.Offset + segment.Length) <= int.MaxValue)
                    httpReq.AddRange((int)segment.Offset, (int)(segment.Offset + segment.Length));
                else
                    AddRangeHeaderHack(httpReq.Headers, segment.Offset, (segment.Offset + segment.Length));
            }

            using (HttpWebResponse httpResp = (HttpWebResponse)httpReq.GetResponse())
            {
                #region Validate response headers
                if (expectPartialContent)
                {
                    if (httpResp.StatusCode != HttpStatusCode.PartialContent) // did we get partial content?
                        throw new ProtocolViolationException("Server did not return the requested partial content.");
                    else if (httpResp.ContentLength != segment.Length) // is the length correct?
                        throw new ProtocolViolationException("Server returned incorrect content length.");
                    else if (System.Text.RegularExpressions.Regex.IsMatch(
                        httpResp.Headers[HttpResponseHeader.ContentRange],
                        String.Format("^bytes {0}-[0-9]+/[0-9]+$", segment.Offset)))
                            throw new ProtocolViolationException("Server returned incorrect content range.");
                } 
                #endregion
                
                Stream dataStream = httpResp.GetResponseStream();

                byte[] buffer = new byte[8192];
                long total = 0;
                int count = int.MaxValue;

                do
                {
                    count = dataStream.Read(buffer, 0, buffer.Length);
                    segment.Write(buffer, 0, count);
                    total += count;
                } while (count > 0 && !segment.IsFull);
                
                httpResp.Close();

                return total;
            }
        }

        /// <summary>
        /// Uses Reflections to add the Range header with Int64 values.
        /// </summary>
        /// <param name="headers">WebRequest.Headers collection.</param>
        /// <param name="start">Start of the content.</param>
        /// <param name="end">End of the content.</param>
        private static void AddRangeHeaderHack(WebHeaderCollection headers, long start, long end)
        {
            // Original workaround by Eric Cadwell, code taken from
            // https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=93714
            Type type = headers.GetType();

            System.Reflection.MethodInfo setAddVerified = type.GetMethod("SetAddVerified",
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.FlattenHierarchy
                );

            string rangeHeaderValue = String.Format("bytes={0}-{1}", start, end);

            if (setAddVerified != null)
                setAddVerified.Invoke(headers, new object[] { "Range", rangeHeaderValue }); 
        }
    }
}
